﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.IO;
using System.Net;
using System.Text.RegularExpressions;
using IGolovkine.Core.Diagnostics;
using IGolovkine.Core;
using System.Threading;
using System.Reflection;

namespace IGolovkine.ShoutCastRipper
{
    public class StationConnection : IStationConnection
    {
        private static readonly TimeSpan ReadTimeout = TimeSpan.FromSeconds(30);
        
        private readonly Stream _stream;
        private readonly int _dataLength;
        private string _currentContentTitle;
        private volatile bool _isClosed;

        public StationConnection(string url, string userAgent)
        {
            Debug.NotNullOrEmpty(() => url);
            Debug.NotNullOrEmpty(() => userAgent);

            HttpWebRequest request = request = (HttpWebRequest) WebRequest.Create(url);
            request.Headers.Clear();
            request.Headers.Add("GET", "HTTP/1.0");
            request.Headers.Add("Icy-MetaData", "1");
            request.UserAgent = userAgent;

            HttpWebResponse response = (HttpWebResponse)request.GetResponse();

            if (response.StatusCode != HttpStatusCode.OK)
            {
                throw new IOException(string.Format("Invalid status code '{0}'", response.StatusDescription));
            }

            _dataLength = GetIcyDataLength(response);
            if (_dataLength == 0)
            {
                throw new IOException("icy-metaint field is 0");
            }

            _stream = response.GetResponseStream();
            _stream.ReadTimeout = Convert.ToInt32(ReadTimeout.TotalMilliseconds);
        }

        private static int GetIcyDataLength(HttpWebResponse response)
        {
            try
            {
                return Convert.ToInt32(response.GetResponseHeader("icy-metaint"));
            }
            catch (Exception ex)
            {
                throw new IOException("Error reading icy-metaint", ex);
            }
        }
        
        public string CurrentContentTitle
        {
            get { return _currentContentTitle; }
        }
        
        public byte[] ReadContentData()
        {
            try
            {
                return MethodHelper.InvokeWithTimeout(ReadTimeout, () => this.InternalReadContentData());
            }
            catch (TimeoutException ex)
            {
                throw new TimeoutException(string.Format("'{0}' has timed out", MethodBase.GetCurrentMethod().Name), ex);
            }
        }

        private byte [] InternalReadContentData()
        {
            this.EnsureNotClosed();
            byte[] data = new byte[_dataLength];
            string newTitle = null;
            do
            {
                this.FillBuffer(data);
                newTitle = this.ReadContentTitle();
                if (newTitle != null)
                {
                    _currentContentTitle = newTitle;
                }
            }
            while (newTitle == null && _currentContentTitle == null);
            return data;
        }

        private string ReadContentTitle()
        {
            int metaDataLength = this.ReadMetaDataLength();
            if (metaDataLength != 0)
            {
                byte[] headerData = new byte[metaDataLength];
                this.FillBuffer(headerData);
                string headerString = Encoding.Default.GetString(headerData);
                IDictionary<string, string> header = ParseContentHeader(headerString);
                return header["StreamTitle"];
            }
            return null;
        }
        
        private int ReadMetaDataLength()
        {
            this.EnsureNotClosed();
            int b = _stream.ReadByte();
            if (b != -1)
            {
                return b * 16;
            }
            else
            {
                throw new IOException("Error reading Meta Data length from the stream");
            }
        }

        private void FillBuffer(byte[] buffer)
        {
            int totalBytesRead = 0;
            while (totalBytesRead < buffer.Length)
            {
                this.EnsureNotClosed();
                int bytesRead = _stream.Read(buffer, totalBytesRead, buffer.Length - totalBytesRead);
                totalBytesRead += bytesRead;
            }
        }

        public bool IsClosed
        {
            get { return _isClosed; }
        }
        
        public void Close()
        {
            if (!_isClosed)
            {
                _isClosed = true;
                _stream.Close();
            }
            else
            {
                throw new InvalidOperationException(string.Format("{0} is already closed", this.GetType().Name));
            }
        }

        public void Dispose()
        {
            if (!_isClosed)
            {
                this.Close();
            }
        }

        private void EnsureNotClosed()
        {
            if (_isClosed)
            {
                throw new StationConnectionClosedException();
            }
        }

        private static IDictionary<string, string> ParseContentHeader(string metadataHeader)
        {
            Dictionary<string, string> headerValues = new Dictionary<string, string>(2);
            foreach (string token in metadataHeader.Split(';'))
            {
                string [] tokens = token.Split('=');
                if (tokens.Length >= 2)
                {
                    headerValues.Add(tokens[0], RemoveQuoteEnclosure(tokens[1].Trim()));
                }                    
            }
            return headerValues;
        }

        private static string RemoveQuoteEnclosure(string value)
        {
            if (value.StartsWith("\'"))
            {
                value = value.Substring(1, value.Length - 1);
            }
            if (value.EndsWith("\'"))
            {
                value = value.Substring(0, value.Length - 1);
            }
            return value;
        }
    }
}
